/******************************************************************************
 * This program is a 100% Java Email Server.
 ******************************************************************************
 * Copyright (c) 2001-2011, Eric Daugherty (http://www.ericdaugherty.com)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ******************************************************************************
 * For current versions and more information, please visit:
 * http://javaemailserver.sf.net/
 *
 * or contact the author at:
 * java@ericdaugherty.com
 * andreaskyrmegalos@hotmail.com
 *
 ******************************************************************************
 * This program is based on the CSRMail project written by Calvin Smith.
 * http://crsemail.sourceforge.net/
 ******************************************************************************
 *
 * $Rev$
 * $Date$
 *
 ******************************************************************************/

package com.ericdaugherty.mail.server.configuration;

//Java imports
import java.io.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.*;
import javax.security.auth.login.*;
import javax.security.auth.Subject;
import javax.swing.JOptionPane;

//Log imports
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

//Encoding Imports
import org.apache.commons.codec.binary.Base64;

//Local imports
import com.ericdaugherty.mail.server.Mail;
import com.ericdaugherty.mail.server.auth.AuthContext;
import com.ericdaugherty.mail.server.auth.LoginConfig;
import com.ericdaugherty.mail.server.configuration.cbc.CBCExecutor;
import com.ericdaugherty.mail.server.errors.InvalidAddressException;
import com.ericdaugherty.mail.server.info.*;
import com.ericdaugherty.mail.server.persistence.LocalDeliveryFactory;
import com.ericdaugherty.mail.server.persistence.LocalDeliveryProcessor;
import com.ericdaugherty.mail.server.security.JESSecurityManager;
import com.ericdaugherty.mail.server.services.smtp.SMTPMessage;
import com.ericdaugherty.mail.server.services.smtp.SMTPMessageImpl;
import com.ericdaugherty.mail.server.utils.*;

/**
 * Provides a centralized repository for all configuration
 * information.
 * <p>
 * All configuration information should be retrieved here for
 * every use.  The ConfigurationManager will reload
 * configuration changes dynamically.
 * <p>
 * Classes may cache the reference to the ConfigurationManager instance,
 * as only one will ever be created.
 *
 * @author Eric Daugherty
 * @author Andreas Kyrmegalos (2.x branch)
 */
public class ConfigurationManager implements ConfigurationParameterConstants {

    /** Logger */
    private static Log log = LogFactory.getLog( ConfigurationManager.class );

    //***************************************************************
    // Variables
    //***************************************************************

    //
    // Internal Control Variables
    //

    private static ConfigurationManager instance;

    private static ConfigurationFileWatcher watcher;

    private static volatile boolean running;

    /**
     * This is used to prevent reseting various settings
     * when mail.conf is updated
     *
     */
    private boolean fixedDefined;

    volatile boolean updatingFiles;

    volatile boolean disableNotify;

    //These fields are package-private
    static boolean complete;
    static boolean abort;

    private ConfigurationManagerBackEnd backEnd;

    private BackEndTypeEnum backEndType;

    private List<String> dirRequests;

    /** The file reference to the mail.conf configuration file */
    private File generalConfigurationFile;

    /** The timestamp for the mail.conf file when it was last loaded */
    private long generalConfigurationFileTimestamp;

    /** The file reference to the rcptPolicy.conf configuration file */
    private File rcptPolicyFile;
    
    /** The file reference to the reverseDNS.conf configuration file */
    private File reverseDNSFile;

    /** The timestamp for the reverseDNS.conf file when it was last loaded */
    private long reverseDNSFileTimestamp;

    /** Controls whether or not debugging output for SSL is desired */
    private boolean debugSSL;

    //
    // Configuration Properties
    //

    public enum TransferMode {
       FULL, LOCAL, REMOTE, TESTING
    }

    public TransferMode transferMode;

    public enum RetrievalMode {
       POP3, NONE
    }

    public RetrievalMode retrievalMode;

    /** The root directory of the application. */
    private String rootDirectory;
    
    /** A directory used for doBackup purposes. */
    private String backupDirectory;
    
    /** The directory used to store the incoming e-mails. */
    private String smtpDirectory;

    /** The directory used to store the user accounts. */
    private String usersDirectory;

    /** The root directory used to store the server certificate and private key. **/
    private String securityDirectory;

    /** The directory used to store the failed e-mails. */
    private String failedDirectory;
    
    /** The local IP address to receive commands on. */
    private InetAddress configurationAddress;
    
    /** The port the configuration address will use. */
    private int configurationPort;

    /** If true uses SSL/TLS to secure the connection to the CBC */
    private boolean configurationSecure;

    /** The local IP address to listen on.  Null for all addresses */
    private InetAddress listenAddress;

    /** The port the SMTP server listens on. */
    private int smtpPort;

    /** The port the POP3 server listens on */
    private int pop3Port;

    /** The number of threads to use for each non secure listener */
    private int executeThreadCount;

    /** The maximum size (in megabytes) allowed for email attachments. */
    private int maximumMessageSize;

    /** The number of seconds to wait between delivery attempts */
    private long deliveryIntervalSeconds;

    /**
     * The max number of delivery attempts before message is considered
     * 'undeliverable' and moved to 'failed' folder
     */
    private int deliveryAttemptThreshold;

    /** True if all outgoing mail should go though the default server */
    private boolean defaultSMTPServerEnabled;

    /** The servers to send all outgoing mail through */
    private DefaultSMTPServer[] defaultSMTPServers;

    /** True if POP Before SMTP is enabled */
    private boolean enablePOPBeforeSMTP;

    /** The timeout length for authenticated ip addresses */
    private long authenticationTimeoutMilliseconds;

    /** IP Addresses that are allowed to relay mail. */
    private String[] relayApprovedIPAddresses;

    /** Email Addresses that are allowed to relay mail. */
    private String[] relayApprovedEmailAddresses;

    /** A flag to control the 8BITMIME feature **/
    private boolean mime8bit;

    //
    // Secure connection variables
    //

    /** Controls whether or not the secure server modules will be activated */
    private boolean secureActive;

    /** The port the secure SMTP server listens on. */
    private int secureSMTPPort;

    /** The port the secure POP3 server listens on */
    private int securePOP3Port;

    /** The number of threads to use for each secure listener */
    private int secureExecuteThreadCount;

    /** The keystore complete path */
    private String keystoreLocation;

    /** The keystore provider */
    private String keystoreProvider;

    /** The keystore type */
    private String keystoreType;

    /** The truststore complete path */
    private String truststoreLocation;

    /** The truststore provider */
    private String truststoreProvider;

    /** The truststore type */
    private String truststoreType;

    private static boolean limitedCryptography = true;

    private static String[] enabledCiphers;

    private SSLSocketFactory sslSocketFactory;

    //
    // Server Security Policy Variables
    //

    private static boolean securityManagerEnabled;

    private boolean enableHELO;

    private boolean notifyDefault;

    private boolean disableWarning;

    private boolean externalDelegated;

    /** Defines the client-auth type */
    private String clientAuth;

    private boolean outgoingSecure;

    /** Controls whether or not the standard server modules will use TLS/SSL */
    private boolean standardSMTPsecure;
    private boolean standardPOP3secure;
    
    /** Controls whether or not clear text password are allowed for SMTP sessions */
    private int allowClearTextSMTP;

    /** Controls whether or not clear text password are allowed for POP3 sessions */
    private int allowClearTextPOP3;

    private boolean nonExistentLocalRejected;

    private int maxErrorCount;

    private int maxPassAttempts;

    private int maxValidRCPT;

    private int addPctRCPT;

    private int minTotFailRCPT;

    private int minPctFailRCPT;

    private boolean verifyIP;

    private String saslQOP;

    private boolean digestMD5Enabled;

    private String digestMD5Ciphers;

    private boolean gssEnabled;

    private String gssRealm;

    private String gssKDC;

    private String gssPrincipal;

    private Map<String, String> gssOptions;

    private AuthContext authContext;

    //
    // amavisd-new variables
    //
    
    /** A flag for amavisd-new support **/
    private boolean amavisSupportActive;

    /** The Transmitting MTA's SMTP directory **/
    private String amavisSMTPDirectory;

    /** The local IP address amavisd-new uses to listen on.  Null for all addresses */
    private InetAddress amavisListenAddress;

    /** The port amavisd-new listens on. */
    private int amavisSMTPPort;

    /** The port JES uses to listens on for amavisd-new filtered messages. */
    private int amavisFilteredSMTPPort;

    //
    // recipient policy variables
    //
    private Map<Object,RcptPolicyArray<String>> rcptPolicyMap;

    //
    // Testing Grounds variables
    //

    private boolean localTestingMode;

    private String testingDirectory;

    //
    // Other variables
    //

    /** Array of domains for which the SMTP server should alter the Hello Domain **/
    private String[] reverseDNSservers;

    private SSLContext sslContext;

    private final Map<String,Integer> mappedPorts = new HashMap<String,Integer>();

    private static Map<String,char[]> pwds = new HashMap<String,char[]>();

    final Locale englishLocale = Locale.ENGLISH;

    //***************************************************************
    // Constructor
    //***************************************************************

    /**
     * Initialize the file path.  Enforces the Singleton pattern.
     *
     * @param generalConfigurationFile the file to load the general configuration from.
     * @param userConfigurationFile the file to load the user configuration from.
     * @param reverseDNSFile the file to load the reverse DNS servers from.
     */
    private ConfigurationManager( File generalConfigurationFile, File reverseDNSFile, File rcptPolicyFile)
    {
        this.generalConfigurationFile = generalConfigurationFile;
        this.reverseDNSFile = reverseDNSFile;
        this.rcptPolicyFile = rcptPolicyFile;
        
    }

    //***************************************************************
    // Static Methods
    //***************************************************************

    /**
     * Initializes the ConfigurationManager to use the specified
     * directory.  This method should only be called once during
     * startup, and then never again.  The file path can not
     * be re-initialized!
     *
     * @param rootDirectory the directory JES was installed into.
     * @return returns the singleton instance of the ConfigurationManager.
     */
    public static ConfigurationManager initialize( String rootDirectory ) {
       
        String generalConfigFilename = "mail.conf";
        String reverseDNSFilename = "reverseDNS.conf";
        String rcptPolicyFilename = "rcptPolicy.conf";

        // Make sure we are not already configured.
        if( instance != null )
        {
            throw new RuntimeException( "Configurationmanager:initialize() called more than once!" );
        }

        // Verify the General config file exists.
        File generalConfigFile = new File( rootDirectory+File.separator+"conf", generalConfigFilename );
        if( !generalConfigFile.isFile() || !generalConfigFile.exists() )
        {
            throw new RuntimeException( "Invalid mail.conf ConfigurationFile! " + generalConfigFile.getAbsolutePath() );
        }
        
        // Verify the ReverseDNS config file exists.
        File userreverseDNSFile = new File( rootDirectory+File.separator+"conf", reverseDNSFilename );
        if( !userreverseDNSFile.isFile() || !userreverseDNSFile.exists() )
        {
            throw new RuntimeException( "Invalid reverseDNS.conf ConfigurationFile! " + userreverseDNSFile.getAbsolutePath() );
        }

        // Verify the Recipient Policy config file exists.
        File rcptPolicyFile = new File( rootDirectory+File.separator+"conf", rcptPolicyFilename );
        if( !rcptPolicyFile.isFile() || !rcptPolicyFile.exists()  )
        {
            throw new RuntimeException( "Invalid rcptPolicy.conf ConfigurationFile! " + rcptPolicyFile.getAbsolutePath() );
        }

        // Go ahead and create the singleton instance.
        instance = new ConfigurationManager( generalConfigFile, userreverseDNSFile, rcptPolicyFile );

        instance.dirRequests = new ArrayList<String>(4);
        instance.setRootDirectory( rootDirectory );
        instance.setSecurityDirectory( rootDirectory+File.separator+"security" );
        instance.setBackupDirectory(rootDirectory+File.separator+"backup");
        instance.setSMTPDirectory(rootDirectory+File.separator+"smtp");
        instance.setUsersDirectory(rootDirectory+File.separator+"users");
        instance.setFailedDirectory(rootDirectory+File.separator+"failed");


        System.setProperty("jes.install.directory", rootDirectory);
        
        // Load the properties from disk.
        instance.loadProperties();

        if (Mail.testing) {
           instance.mappedPorts.put("testing", instance.getSMTPPort()+1);
           File testingDir = new File(rootDirectory, "testing");
           if (!testingDir.exists()) {
              testingDir.mkdir();
           }
           instance.amavisSMTPDirectory = testingDir.getPath();
           try {
              ((JESSecurityManager)System.getSecurityManager()).updateMappedPorts();
           }
           catch (NullPointerException npe) {
              //A security manager may not be active yet
           }
        }

        List<String> principals = new ArrayList<String>(2);
        if (instance.gssEnabled) {
           System.setProperty("java.security.krb5.realm", instance.gssRealm );
           System.setProperty("java.security.krb5.kdc", instance.gssKDC );
           Configuration.setConfiguration(new LoginConfig("com.ericdaugherty.mail.server.auth.GSSServerMode", instance.gssOptions));
           String principal = instance.gssPrincipal;
           if (principal.substring(0,principal.indexOf(':')).equals(principal.substring(principal.indexOf(':')+1,principal.indexOf('/')))) {
              principals.add(principal.substring(0,principal.indexOf(':')));
              Subject subject = getSubject(principals.get(0));
              instance.authContext = AuthContext.initialize(new Subject[]{subject});
              principals.add(principals.remove(0).toLowerCase());
           }
           else {
              principals.add(principal.substring(0,principal.indexOf(':')));
              Subject subject1 = getSubject(principals.get(0));
              principals.add(principals.remove(0).toLowerCase());
              principals.add(principal.substring(principal.indexOf(':')+1,principal.indexOf('/')));
              Subject subject2 = getSubject(principals.get(1));
              principals.add(principals.remove(1).toLowerCase());
              instance.authContext = AuthContext.initialize(new Subject[]{subject1,subject2});
           }
        }

        savePasswords(principals);
        principals.clear();

        return instance;
    }

    static void addIdentityPassword(String principal, char[] pwd) {
       
       pwds.put(principal, pwd.clone());
    }

    private static void savePasswords(List<String> principals) {

       Iterator<String> keys = pwds.keySet().iterator();
       String key;
       char[] pwd;
       while (keys.hasNext()){
          key = keys.next();
          if (key.equals("keystore")) continue;
          if (!principals.contains(key)) {
             pwd = pwds.get(key);
             for (int i=0;i<pwd.length;i++) {
                pwd[i] = 0x00;
             }
             keys.remove();
          }
       }
       File pwdFile = new File(instance.getSecurityDirectory(), "pwds.dat");
       java.security.Key secretKey = null;
       if (pwdFile.exists()) {
          BufferedReader br = null;
          try {
             File sealerFile = new File(instance.getSecurityDirectory(), "pwds.sea");
             br = new BufferedReader(new InputStreamReader(new FileInputStream(sealerFile), "US-ASCII"));
             secretKey = new SecretKeySpec(Base64.decodeBase64(br.readLine().getBytes()), "AES");

          }
          catch (Exception e) {
             log.error(e.getMessage());
             throw new RuntimeException(e.getMessage(),e);
          }
          finally {
             if (br!=null) {
                try {
                   br.close();
                }
                catch (IOException ioe) {}
             }
          }
       }
       else {
          PrintWriter pw = null;
          try {
             KeyGenerator kg = KeyGenerator.getInstance("AES");
             kg.init(limitedCryptography?128:256);
             secretKey = kg.generateKey();
             File sealerFile = new File(instance.getSecurityDirectory(), "pwds.sea");
             pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(sealerFile), "US-ASCII"));
             pw.println(new String(Base64.encodeBase64(secretKey.getEncoded()),"US-ASCII"));
             String os = System.getProperty("os.name").toLowerCase();
             if (os.contains("win")) {
                try {
                   new ProcessBuilder( "cmd.exe", "/C", "cacls "+instance.getSecurityDirectory()+
                        File.separator+"pwds.sea /e /r Users" ).redirectErrorStream(true).start();
                } catch (IOException e) {if (log.isDebugEnabled())log.debug(e.getMessage(), e);}
             }
             else if (os.contains("mac")) {
                //No macOS unit to test this on
                try {
                   new ProcessBuilder( "chmod o-rw "+instance.getSecurityDirectory()+
                        File.separator+"pwds.sea" ).redirectErrorStream(true).start();
                } catch (IOException e) {if (log.isDebugEnabled())log.debug(e.getMessage(), e);}
             }
             else {
                try {
                   new ProcessBuilder( "bash", "-c", "chmod o-rw "+instance.getSecurityDirectory()+
                        File.separator+"pwds.sea" ).redirectErrorStream(true).start();
                } catch (IOException e) {if (log.isDebugEnabled())log.debug(e.getMessage(), e);}
             }

          }
          catch (Exception e) {
             log.error(e.getMessage());
             throw new RuntimeException(e.getMessage(),e);
          }
          finally {
             if (pw!=null) {
                pw.close();
             }
          }
       }

       ObjectOutputStream oos = null;
       try {
          Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
          cipher.init(Cipher.ENCRYPT_MODE, secretKey);
          SealedObject so = new SealedObject((HashMap)pwds, cipher);
          oos = new ObjectOutputStream(new FileOutputStream(pwdFile));
          oos.writeObject(so);
       }
       catch (Exception e) {
          log.error(e.getMessage());
          throw new RuntimeException(e.getMessage(),e);
       }
       finally {
          if (oos!=null) {
             try {
                oos.close();
             }
             catch (IOException ioe) {}
          }
       }
       keys = pwds.keySet().iterator();
       while(keys.hasNext()) {
          pwd = pwds.get(keys.next());
          for (int i=0;i<pwd.length;i++) {
             pwd[i] = 0x00;
          }
       }
       pwds.clear();

    }

    private static Subject getSubject(String principalName) {
        LoginContext lc = null;
        try {
           LoginCallbackHandler cbh = null;
           //A password is needed, check if one exists in the passwords file
           if (pwds.containsKey(principalName.toLowerCase())) {
              cbh = new LoginCallbackHandler();
              cbh.setPassword(pwds.get(principalName.toLowerCase()).clone());
           }
           //Not present in passwords file, will require a gui
           else {
              //If the jvm is headless throw exception
              if (java.awt.GraphicsEnvironment.isHeadless()) {
                 log.error("The JVM is in headless mode but a GUI is required");
                 throw new RuntimeException("A GUI is required");
              }
              cbh = new LoginCallbackHandlerGUI(principalName);
           }
           lc = new LoginContext("com.ericdaugherty.mail.server.auth.GSSServerMode"+principalName, cbh);
        } catch (LoginException le) {
           throw new RuntimeException("Cannot create LoginContext. "+ le.getMessage(), le);
        } catch (SecurityException se) {
           throw new RuntimeException("Cannot create LoginContext. "+ se.getMessage(), se);
        }
        try {
           lc.login();
           log.info("successful kerberos login");
           lc.getSubject().setReadOnly();
           return lc.getSubject();

        } catch (AccountExpiredException aee) {

           throw new RuntimeException("Your account has expired. Please notify your administrator.",aee);
        } catch (CredentialExpiredException cee) {

           throw new RuntimeException("Your credentials have expired.",cee);
        } catch (FailedLoginException fle) {

           throw new RuntimeException("Authentication Failed",fle);
        } catch (Exception e) {

           throw new RuntimeException("Unexpected Exception - unable to continue",e);
        }

    }

    /**
     * Provides access to the singleton instance.
     *
     * @return the singleton instance.
     */
    public static ConfigurationManager getInstance()
    {
        if( instance == null )
        {
            throw new RuntimeException( "ConfigurationManager can not be accessed before it is initialized!" );
        }
        return instance;
    }

    //***************************************************************
    // Public Methods
    //***************************************************************

    //***************************************************************
    // Parameter Access Methods
    //***************************************************************

    public static void shutdown() {

       instance.backEnd.shutdown();
       PasswordFactory.shutdown();
       instance = null;
       running = false;
       if (watcher!=null) {
          watcher.interrupt();
       }
       watcher = null;
       securityManagerEnabled = false;
       complete = false;
       abort = false;
    }
    
    public boolean isDebugSSL() {
       return debugSSL;
    }

    public ConfigurationManagerBackEnd getBackEnd() {
       return backEnd;
    }

    /**
     * The type of backend used to store domains, users, realms
     *
     * @return backEndType BackEndTypeEnum
     */
    public BackEndTypeEnum getBackEndType() {
       return backEndType;
    }

    public TransferMode getTransferMode() {
       return transferMode;
    }

    public RetrievalMode getRetrievalMode() {
       return retrievalMode;
    }

    /**
     * The root directory used to store the incoming and outgoing messages.
     *
     * @return String
     */
    public String getRootDirectory() {
        return rootDirectory;
    }

    /**
     * The root directory used to store the incoming and outgoing messages.
     *
     * @param rootDirectory String
     */
    private void setRootDirectory(String rootDirectory) {
        this.rootDirectory = rootDirectory;
    }

    /**
     * The directory used to store doBackup files.
     *
     * @return backupDirectory String
     */
    public String getBackupDirectory() {
        return backupDirectory;
    }
    
    /**
     * The directory used to store doBackup files.
     *
     * @param backupDirectory String
     */
    private void setBackupDirectory(String backupDirectory) {
        this.backupDirectory = backupDirectory;
        File backup = new File(backupDirectory);
        if (!backup.exists()) {
           requestDirCreation("backup");
        }
    }
    
    /**
     * The directory used to store incoming e-mails.
     * 
     * @return SMTPDirectory String
     */
    public String getSMTPDirectory() {
       return smtpDirectory;
    }
        
    /**
     * The directory used to store incoming e-mails.
     *
     * @param smtpDirectory String
     */
    private void setSMTPDirectory(String smtpDirectory) {
        this.smtpDirectory = smtpDirectory;
        File smtpDir = new File(smtpDirectory);
        if( !smtpDir.exists() ) {
           requestDirCreation("smtp");
           return;
        }
        //If any incomplete temporary messages are present at application initialiazation delete them.
        File[] files = new File(smtpDirectory).listFiles(new FileFilter(){

           public boolean accept(File file) {
              if (file.getName().toLowerCase().endsWith(".tmp")) return true;
              return false;
           }
        });
        int numFiles = files.length;
        for (int i=0;i<numFiles;i++) {
           files[i].delete();
        }
    }

    /**
     * The directory used to store the user accounts.
     *
     * @return String
     */
    public String getUsersDirectory() {
       return usersDirectory;
    }

    /**
     * The directory used to store the user accounts.
     *
     * @param mailDirectory String
     */
    private void setUsersDirectory(String usersDirectory) {
        this.usersDirectory = usersDirectory;
        File usersDir = new File(usersDirectory);
        // If the directory does not exist, create it.
        if( !usersDir.exists() ) {
           requestDirCreation("users");
        }
    }
    
    /**
     * The root directory used to store the server certificate and private key.
     * 
     * @return securityDirectory String
     */
    public String getSecurityDirectory() {
       return securityDirectory;
    }

    /**
     * The root directory used to store the server certificate and private key.
     *
     * @param securityDirectory String
     */
    private void setSecurityDirectory(String securityDirectory) {
        this.securityDirectory = securityDirectory;
    }

    /**
     * The directory used to store failed e-mails.
     *
     * @return failedDirectory String
     */
    public String getFailedDirectory() {
       return failedDirectory;
    }

    /**
     * The directory used to store failed e-mails.
     *
     * @param failedDirectory String
     */
    private void setFailedDirectory(String failedDirectory) {
        this.failedDirectory = failedDirectory;
        File failedDir = new File(failedDirectory);
        // If the directory does not exist, create it.
        if( !failedDir.exists() ) {
           requestDirCreation("failed");
        }
    }

    public void requestDirCreation(String directory) {
       if (dirRequests.contains(directory)) return;
       if (log.isDebugEnabled()) {
          log.debug("adding request to create directory "+directory);
       }
       dirRequests.add(directory);
    }

    public void createDirectories() {
       if (dirRequests.isEmpty()) return;
       if (log.isDebugEnabled()) {
          log.debug("Creating Directories");
       }
       if (dirRequests.contains("users")) {
          dirRequests.remove("users");
          File usersDir = new File(getUsersDirectory());
          if (!usersDir.exists()) {
             log.info( "Users directory does not exist.  Creating: " + usersDir.getAbsolutePath() );
             if( !usersDir.mkdir() )
             {
                log.error( "Error creating users directory: " + usersDir.getAbsolutePath() + ". No mail will be delivered to local users!" );
                throw new RuntimeException( "Unable to create users Directory." );
             }
          }
       }
       Iterator<String> iter = dirRequests.iterator();
       String directory;
       LocalDeliveryProcessor ldp = LocalDeliveryFactory.getInstance().getLocalDeliveryProccessor();
       while(iter.hasNext()) {
          directory = iter.next();
          if (directory.equals("smtp")) {
             File smtpDir = new File(getSMTPDirectory());
             if (smtpDir.exists()) continue;
             log.info( "SMTP Mail directory does not exist.  Creating: " + smtpDir.getAbsolutePath() );
             if( !smtpDir.mkdir() )
             {
                log.error( "Error creating SMTP Mail directory: " + smtpDir.getAbsolutePath() + ". No incoming mail will be accepted!" );
                throw new RuntimeException( "Unable to create SMTP Mail Directory." );
             }
          }
          else if (directory.equals("amavis")) {
             File amavisDir = new File(getAmavisSMTPDirectory());
             if (amavisDir.exists()) continue;
             log.info( "Amavis SMTP Mail directory does not exist.  Creating: " + amavisDir.getAbsolutePath() );
             if( !amavisDir.mkdir() )
             {
                log.error( "Error creating Amavis SMTP Mail directory: " + amavisDir.getAbsolutePath() + ". No incoming mail will be accepted!" );
                throw new RuntimeException( "Unable to create Amavis SMTP Mail Directory." );
             }
          }
          else if (directory.equals("backup")) {
             File backup = new File(getBackupDirectory());
             if( !backup.mkdir() )
             {
                log.error( "Error creating backup directory: " + backup.getAbsolutePath() + "." );
                throw new RuntimeException( "Unable to create backup Directory." );
             }
          }
          else if (directory.equals("failed")) {
             File failedDir = new File(getFailedDirectory());
             log.info( "failed directory does not exist.  Creating: " + failedDir.getAbsolutePath() );
             if( !failedDir.mkdir() )
             {
                log.error( "Error creating failed directory: " + failedDir.getAbsolutePath() + ".  No failed mail will be stored!" );
                throw new RuntimeException( "Unable to create failed Directory." );
             }
          }
          else if (directory.equals("external")) {
             new File(instance.getRootDirectory(), "external").mkdir();
          }
          else if (directory.startsWith("testing:")) {
             String dirToCreate = directory.substring(directory.indexOf(':')+1);
             File testingDirectory = new File(dirToCreate);
             if (!testingDirectory.exists()) {
                if( !testingDirectory.mkdir() )
                {
                   log.error( "Error creating testing directory: " + testingDirectory.getAbsolutePath() + "." );
                   throw new RuntimeException( "Unable to create testing Directory." );
                }
             }
          }
          else {
             ldp.createUserRepository(directory);
          }
       }
       dirRequests.clear();
    }

    public InetAddress getConfigurationAddress() {
        return configurationAddress;
    }

    public int getConfigurationPort() {
        return configurationPort;
    }

    public boolean isConfigurationSecure() {
        return configurationSecure;
    }

    /**
     * The local IP address to lisen on.  Null for all addresses
     *
     * @return null for all addresses.
     */
    public InetAddress getListenAddress() {
        return listenAddress;
    }

    /**
     * The port the SMTP server listens on.
     *
     * @return port number
     */
    public int getSMTPPort() {
        return smtpPort;
    }

    /**
     * The port the POP3 server listens on.
     *
     * @return port number
     */
    public int getPOP3Port() {
        return pop3Port;
    }

    /**
     * The number of threads to use for each listener.
     *
     * @return int
     */
    public int getExecuteThreadCount() {
        return executeThreadCount;
    }

    /** The maximum size (in megabytes) allowed for email attachments. */
    public int getMaximumMessageSize() {
        return maximumMessageSize;
    }

    /** The number of seconds to wait between delivery attempts */
    public long getDeliveryIntervalSeconds() {
        return deliveryIntervalSeconds;
    }

    /** The number of milliseconds to wait between delivery attempts */
    public long getDeliveryIntervealMilliseconds()
    {
        return deliveryIntervalSeconds * 1000;
    }

    /** The number of seconds to wait between delivery attempts */
    private void setDeliveryIntervalSeconds(long deliveryIntervalSeconds) {
        this.deliveryIntervalSeconds = deliveryIntervalSeconds;
    }

    /**
     * Get the max number of delivery attempts before message is considered
     * 'undeliverable' and moved to 'failed' folder
     * @return int deliveryAttemptThreshold
     */
    public int getDeliveryAttemptThreshold() {
        return deliveryAttemptThreshold;
    }

    /**
     * Checks the local domains to see if the specified
     * parameter matches.
     *
     * @param domain a domain to check.
     * @return true if and only if it matches exactly an existing domain.
     */
    public synchronized boolean isLocalDomain( String domain )
    {
       
        return backEnd.isLocalDomain(new Domain(domain));
    }

    public synchronized EmailAddress getDefaultMailbox(String domain) {
       if (domain==null) return null;
        EmailAddress defaultAddress = backEnd.getDefaultMailbox(new Domain(domain.toLowerCase(englishLocale)));
        if(defaultAddress == null){
        	try {
				defaultAddress = new EmailAddress("test@example.com");
			} catch (InvalidAddressException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
        }
        return defaultAddress;
    }

    /** True if all outgoing mail should go though the default server */
    public boolean isDefaultSmtpServerEnabled() {
        return defaultSMTPServerEnabled;
    }

    /** The servers to send all outgoing mail through */
    public DefaultSMTPServer[] getDefaultSmtpServers() {
        return defaultSMTPServers;
    }

    /** True if POP Before SMTP is a valid relay option */
    public boolean isEnablePOPBeforeSMTP() {
        return enablePOPBeforeSMTP;
    }


    /** The timeout length for authenticated ip addresses */
    public long getAuthenticationTimeoutMilliseconds() {
        return authenticationTimeoutMilliseconds;
    }

    /** The timeout length for authenticated ip addresses */
    private void setAuthenticationTimeoutMinutes(long minutes) {
        this.authenticationTimeoutMilliseconds = minutes * 60 * 1000;
    }

    /** IP Addresses that are allowed to relay mail. */
    public String[] getRelayApprovedIPAddresses() {
        return relayApprovedIPAddresses;
    }

    /** IP Addresses that are allowed to relay mail. */
    private void setRelayApprovedIPAddresses(String[] relayApprovedIPAddresses) {
        this.relayApprovedIPAddresses = relayApprovedIPAddresses;
    }

    /** Email Addresses that are allowed to relay mail. */
    public String[] getRelayApprovedEmailAddresses() {
        return relayApprovedEmailAddresses;
    }

    /** Emails Addresses that are allowed to relay mail. */
    private void setRelayApprovedEmailAddresses(String[] relayApprovedEmailAddresses) {
        this.relayApprovedEmailAddresses = relayApprovedEmailAddresses;
    }

    /**
     * A flag to indicate if 8BITMIME is to be used
     *
     * @return boolean mime8bit
     */
    public boolean is8bitMIME() {
       return mime8bit;
    }

    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * Check whether the secure POP3 modules are active/to be activated.
     *
     * @return boolean standardsecure
     */
    public boolean isSecureActive() {
       return secureActive;
    }

    /**
     * The port the secure SMTP server listens on.
     *
     * @return int secureSMTPPort
     */
    public int getSecureSMTPPort() {
        return secureSMTPPort;
    }

    /**
     * The port the secure POP3 server listens on.
     *
     * @return port number
     */
    public int getSecurePOP3Port() {
        return securePOP3Port;
    }

    /**
     * The number of threads to use for each secure listener.
     *
     * @return int
     */
    public int getSecureExecuteThreadCount() {
        return secureExecuteThreadCount;
    }

    /**
     * The location of the keystore containg the server's certificates and
     * private key.
     *
     * @return String keystoreLocation
     */
    public String getKeystoreLocation() {
       return keystoreLocation;
    }

    /**
     * The provider of the keystore containg the server's certificates and
     * private key.
     *
     * @return String keystoreProvider
     */
    public String getKeystoreProvider() {
       return keystoreProvider;
    }

    /**
     * The type of the keystore containg the server's certificates and
     * private key.
     *
     * @return String keystoreType
     */
    public String getKeystoreType() {
       return keystoreType;
    }

    /**
     * The location of the truststore containg the server's certificates and
     * private key.
     *
     * @return String truststoreLocation
     */
    public String getTruststoreLocation() {
       return truststoreLocation;
    }

    /**
     * The provider of the truststore containg the server's certificates and
     * private key.
     *
     * @return String truststoreProvider
     */
    public String getTruststoreProvider() {
       return truststoreProvider;
    }

    /**
     * The type of the truststore containg the server's certificates and
     * private key.
     *
     * @return String truststoreType
     */
    public String getTruststoreType() {
       return truststoreType;
    }

    public SSLSocketFactory getSSLSocketFactory() {
       return sslSocketFactory;
    }

    public String[] getEnabledCiphers() {
       return enabledCiphers;
    }

    ///////////////////////////////////////////////////////////////////////////////////
    
    public static boolean isSecurityManagerEnabled() {
       return securityManagerEnabled;
    }

    public boolean isHELOEnabled() {
       return enableHELO;
    }
    
    /**
     * Check whether the standard SMTP/POP3 modules are to use TLS/SSL security.
     * 
     * @return boolean standardsecure
     */
    public String getClientAuth() {
       return clientAuth;
    }

    /**
     * Check whether to use TLS/SSL for an outbound SMTP message
     *
     * @return boolean outgoingSecure
     */
    public boolean isOutgoingSecure() {
       return outgoingSecure;
    }
    
    /**
     * Check whether the standard SMTP modules are to use TLS/SSL security.
     *
     * @return boolean standardSMTPsecure
     */
    public boolean isStandardSMTPSecure() {
       return standardSMTPsecure;
    }

    /**
     * Check whether the standard POP3 modules are to use TLS/SSL security.
     *
     * @return boolean standardPOP3secure
     */
    public boolean isStandardPOP3Secure() {
       return standardPOP3secure;
    }
    
    /**
     * Check whether or not clear text passwords are allowed in SMTP sessions.
     * 
     * @return int allowClearText
     */
    public int allowClearTextSMTP() {
       return allowClearTextSMTP;
    }
    
    /**
     * Check whether or not clear text passwords are allowed in POP3 sessions.
     * 
     * @return int allowClearText
     */
    public int allowClearTextPOP3() {
       return allowClearTextPOP3;
    }

    public boolean isNonExistentLocalRejected() {
       return nonExistentLocalRejected;
    }

    /**
     * The maximum number of errors during a POP3 or SMTP server session.
     * 
     * @return int maxErrorCount
     */
    public int getMaxErrorCount() {
       return maxErrorCount;
    }

   public int getMaxPassAttempts() {
      return maxPassAttempts;
   }

   public int getMaxValidRCPT() {
      return maxValidRCPT;
   }

   public int getAddPctRCPT() {
      return addPctRCPT;
   }

   public int getMinTotFailRCPT() {
      return minTotFailRCPT;
   }

   public int getMinPctFailRCPT() {
      return minPctFailRCPT;
   }
   public boolean isVerifyIP() {
      return verifyIP;
   }

    public String getSaslQOP() {
       return saslQOP;
    }

    public String getDigestMD5Ciphers() {
       return digestMD5Ciphers;
    }

    public boolean isDigestMD5Enabled() {
       return digestMD5Enabled;
    }

    public boolean isGSSEnabled() {
       return gssEnabled;
    }

    public String getGSSPrincipal() {
       return gssPrincipal;
    }

    ///////////////////////////////////////////////////////////////////////////////////

    public boolean isAmavisSupportActive() {
       return amavisSupportActive;
    }

    public String getAmavisSMTPDirectory() {
       return amavisSMTPDirectory;
    }

    private void setAmavisSMTPDirectory(String amavisSMTPDirectory) {
       this.amavisSMTPDirectory = amavisSMTPDirectory;
        File amavisDir = new File(amavisSMTPDirectory);
        // If the directory does not exist, create it.
        if( !amavisDir.exists() ) {
           requestDirCreation("amavis");
        }
    }

    public InetAddress getAmavisListenAddress() {
       return amavisListenAddress;
    }

    public int getAmavisSMTPPort() {
       return amavisSMTPPort;
    }

    public int getAmavisFilteredSMTPPort() {
       return amavisFilteredSMTPPort;
    }

    ///////////////////////////////////////////////////////////////////////////////////

    public Map<Object,RcptPolicyArray<String>> getRcptPolicyMap() {
       return rcptPolicyMap;
    }

    ///////////////////////////////////////////////////////////////////////////////////

    public boolean isLocalTestingMode() {
       return localTestingMode;
    }

    public String getTestingDirectory() {
       return testingDirectory;
    }

    ///////////////////////////////////////////////////////////////////////////////////

    public void updateDefaultDomain() {
       
       backEnd.updateDefaultDomain();
    }

    /**
     * Array of domains that the SMTP server should alter the initial greeting reported domain
     *
     * @return String array
     */
    public String[] getReverseDNSservers() {
        return reverseDNSservers;
    }
    
    /**
     * Returns the specified user, or null if the user
     * does not exist.
     *
     * @param address the user's full email address.
     * @return null if the user does not exist.
     */
    public synchronized User getUser( EmailAddress address )
    {
        User user = backEnd.getUser(address);
        if( log.isInfoEnabled() && user == null )
           log.info( "Tried to load non-existent user: " +  address );

        return user;
    }
    
    /**
     * Returns the specified realm, or null if the realm
     * does not exist.
     *
     * @param realmName the realm's full name.
     * @return null if the realm does not exist.
     */
    public synchronized Realm getRealm( String realmName )
    {
       Realm realm = backEnd.getRealm(realmName);
       if( log.isInfoEnabled() && realm == null )
           log.info( "Tried to load non-existent realm: " +  realmName );
       return realm;
    }

    public synchronized String getRealmsForResponse()
    {
       return backEnd.getRealmsForResponse();
    }
    
    public synchronized DomainWithPassword getRealmPassword( String realmName, String username ) {

       return backEnd.getRealmPassword(realmName, username);
    }

    public SSLContext getSSLContext() {
       return sslContext;
    }

    public final Map<String,Integer> getMappedPorts() {
       return Collections.unmodifiableMap(mappedPorts);
    }

    //***************************************************************
    // Private Methods
    //***************************************************************

    private void loadProperties() {

       loadPasswords();
       loadGeneralProperties();
       loadreverseDNSservers();
       backEnd.loadUsersAndRealms();
    }

    private void loadPasswords() {
       File pwdFile = new File(instance.getSecurityDirectory(), "pwds.dat");
       if (pwdFile.exists()) {
          java.security.Key secretKey = null;
          BufferedReader br = null;
          try {
             File sealerFile = new File(instance.getSecurityDirectory(), "pwds.sea");
             br = new BufferedReader(new InputStreamReader(new FileInputStream(sealerFile), "US-ASCII"));
             secretKey = new SecretKeySpec(Base64.decodeBase64(br.readLine().getBytes()), "AES");

          }
          catch (Exception e) {
             log.error(e.getMessage());
             throw new RuntimeException(e.getMessage(),e);
          }
          finally {
             if (br!=null) {
                try {
                   br.close();
                }
                catch (IOException ioe) {}
             }
          }

          ObjectInputStream ois = null;
          try {
             ois = new ObjectInputStream(new FileInputStream(pwdFile));
             SealedObject so = (SealedObject)ois.readObject();
             pwds = (Map<String,char[]>)so.getObject(secretKey);
          }
          catch (Exception e) {
             log.error(e.getMessage());
             throw new RuntimeException(e.getMessage(), e);
          }
          finally {
             if (ois!=null) {
                try {
                   ois.close();
                }
                catch (IOException ioe) {}
             }
          }
       }
       
       pwdFile = new File(instance.getSecurityDirectory(), "password");
       if (pwdFile.exists()) {
           Properties properties = null;
           DelimitedInputStream dis = null;
           try {
               dis = new DelimitedInputStream(new FileInputStream( pwdFile ),2048);
               JESProperties jesProperties = new JESProperties(dis);
               jesProperties.load();
               properties = jesProperties.getProperties();
           }
           catch (IOException e) {
              log.error(e.getMessage());
               throw new RuntimeException(e.getMessage(), e);
           }
           finally {
              if (null!=dis) {
                 try {
                    dis.close();
                 } catch (IOException ex) {
                    if (log.isDebugEnabled()) {
                       log.debug(ex.getMessage());
                    }
                 }
              }
           }
           Enumeration enumeration = properties.keys();
           String key;
           while(enumeration.hasMoreElements()) {
              key = ((String)enumeration.nextElement()).toLowerCase();
              pwds.put(key, properties.getProperty(key).toCharArray());
           }
           pwdFile.delete();
       }
    }

    /**
     * Loads the properties file into the local variables for quick access.
     */
    private void loadGeneralProperties() {
        Properties properties = new Properties();
        
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream( generalConfigurationFile );
            properties.load( inputStream );
        }
        catch (IOException e) {
            // All checks should be done before we get here, so there better
            // not be any errors.  If so, throw a RuntimeException.
            throw new RuntimeException( "Error Loading Properties File!  Unable to continue operation." );
        }
        finally {
           if (inputStream!=null) {
               try {
                  inputStream.close();
               } catch (IOException ex) {
                  if (log.isDebugEnabled()) {
                     log.debug(ex);
                  }
               }
           }
        }

        debugSSL = Boolean.parseBoolean(properties.getProperty( "debugSSL", "false" ).trim());

        if (!fixedDefined) {

           backEndType = BackEndTypeEnum.getBackEndTypeEnum(properties.getProperty( BACKEND, "FILE").trim().toUpperCase(englishLocale));

           switch (backEndType) {
              case FILE: {
                 backEnd = new ConfigurationManagerFile(instance);
              }break;
              case RDBM: {
                 char[] username = properties.getProperty( BACKEND_DB_USERNAME, "").toCharArray();
                 char[] password = properties.getProperty( BACKEND_DB_PASSWORD, "").toCharArray();
                 char[] guiUsername =  properties.getProperty( GUI_DB_USERNAME, "").toCharArray();
                 char[] guiPassword =  properties.getProperty( GUI_DB_PASSWORD, "").toCharArray();

                 if (username.length==0||password.length==0||guiUsername.length==0||guiPassword.length==0) {

                    throw new RuntimeException( "Need to specify username and password for both the backend (full access) and the gui (read only access) to gain access to the database." );
                 }
                 else if (new String(username).equalsIgnoreCase(new String(guiUsername))) {

                    throw new RuntimeException( "Need to specify distinct usernames for the backend and the gui." );
                 }
                 String derbyHome = properties.getProperty( BACKEND_DB_DIRECTORY, rootDirectory);
                 System.setProperty("derby.system.home", derbyHome);
                 System.setProperty("username", new String(username));
                 System.setProperty("password", new String(password));
                 System.setProperty("gui-username", new String(guiUsername));
                 System.setProperty("gui-password", new String(guiPassword));
                 System.setProperty("derby.drda.host", properties.getProperty( BACKEND_DB_HOST, "localhost"));
                 System.setProperty("derby.drda.portNumber", properties.getProperty( BACKEND_DB_PORT, "1527"));
                 System.setProperty("derby.connection.requireAuthentication", "true");
                 System.setProperty("derby.authentication.provider", "com.ericdaugherty.mail.server.configuration.backEnd.DbAuthentication");

                 backEnd = new ConfigurationManagerDb(instance, username, password, Integer.valueOf(properties.getProperty( BACKEND_MINIMUM, "30")),
                       Integer.valueOf(properties.getProperty( BACKEND_MINIMUM, "150")));
                 for(int i=0;i<username.length;i++) {
                    username[i] = 0;
                 }
                 for(int i=0;i<password.length;i++) {
                    password[i] = 0;
                 }
                 for(int i=0;i<guiUsername.length;i++) {
                    guiUsername[i] = 0;
                 }
                 for(int i=0;i<guiPassword.length;i++) {
                    guiPassword[i] = 0;
                 }
              }break;
              case LDAP: {
                 backEnd = new ConfigurationManagerLDAP();
              }break;
              default: {
                 throw new RuntimeException( "The backEnd entry is only allowed the 'file', 'db' and 'LDAP' values." );
              }
           }
       }

        //
        // Load the local domains
        //

        if (backEndType==BackEndTypeEnum.FILE) {
           String domains = properties.getProperty( DOMAINS );
           String defaultUserString = properties.getProperty( DEFAULT_MAILBOX, "" ).trim();
           if( domains == null) {
               throw new RuntimeException( "No Local Domains defined! Can not run without local domains defined." );
           }

           backEnd.updateDomains(domains, defaultUserString);
        }

        PasswordFactory.instantiate(backEndType);

        if (!fixedDefined) {

           String transferModeString = properties.getProperty( MAIL_TRANSFER_MODE, "FULL").trim().toUpperCase(englishLocale);
           transferMode = TransferMode.valueOf(transferModeString);

           String retrievalModeString = properties.getProperty( MAIL_RETRIEVAL_MODE, "POP3").trim().toUpperCase(englishLocale);
           retrievalMode = RetrievalMode.valueOf(retrievalModeString);

           String smtpDirectory = properties.getProperty( SMTPDIRECTORY, "").trim();
           if (!smtpDirectory.equals("")) {
              instance.setSMTPDirectory(smtpDirectory);
           }
           log.info("JES incoming SMTP spool "+instance.getSMTPDirectory());
           System.setProperty("jes.incoming.directory", instance.getSMTPDirectory());
           String usersDirectory = properties.getProperty( USERSDIRECTORY, "").trim();
           if (!usersDirectory.equals("")) {
              instance.setUsersDirectory(usersDirectory);
           }
           System.setProperty("jes.users.directory", instance.getUsersDirectory());
           //The amavis directory property is always given a value to avoid possible security issues
           System.setProperty("jes.amavis.directory", instance.getSMTPDirectory());
           outgoingSecure = properties.getProperty( OUTGOINGSECURE, "false").trim().
                 toLowerCase().equals("true")?true:false;
           if (System.getProperty("wrapper.key")!=null) {
              System.setProperty("user.country",
                    properties.getProperty( LOCALE_COUNTRY ,"US").trim());
              System.setProperty("user.language",
                    properties.getProperty( LOCALE_LANGUAGE ,"en").trim());
              System.setProperty("file.encoding",
                    properties.getProperty( FILE_ENCODING ,"UTF-8").trim());
              System.setProperty("os.name",
                    properties.getProperty( OS_NAME ,"Linux").trim());
           }
           try {
              keystoreLocation = properties.getProperty( KEYSTORELOCATION, instance.getSecurityDirectory()+File.separator+"keystore.jceks").trim();
           }
           catch (Exception ex) {
              keystoreLocation = "";
           }
           try {
              keystoreProvider = properties.getProperty( KEYSTOREPROVIDER, "SunJCE").trim();
           }
           catch (Exception ex) {
              keystoreProvider = "SunJCE";
           }
           try {
              keystoreType = properties.getProperty( KEYSTORETYPE, "JCEKS").trim();
           }
           catch (Exception ex) {
              keystoreType = "JCEKS";
           }
           try {
              truststoreLocation = properties.getProperty( TRUSTSTORELOCATION, "").trim();
              if (truststoreLocation.equals("${jes.install.directory}")) {
                 truststoreLocation = getRootDirectory()+File.separator+"truststore.jks";
              }
           }
           catch (Exception ex) {
              truststoreLocation = "";
           }
           try {
              truststoreProvider = properties.getProperty( TRUSTSTOREPROVIDER, "SUN").trim();
           }
           catch (Exception ex) {
              truststoreProvider = "SUN";
           }
           try {
              truststoreType = properties.getProperty( TRUSTSTORETYPE, "JKS").trim();
           }
           catch (Exception ex) {
              truststoreType = "JKS";
           }

          try {
             limitedCryptography = properties.getProperty( CRYPTOGRAPHY, "LIMITED").toUpperCase(englishLocale).trim().equals("LIMITED");
          }
          catch (Exception ex) {
             limitedCryptography = true;
          }

           enabledCiphers = limitedCryptography?LIMITED_ENABLED_CIPHERS:ENABLED_CIPHERS;

           externalDelegated = Boolean.parseBoolean(properties.getProperty(EXTERNAL_DELEGATED, "false"));

           saslQOP = properties.getProperty(SASL_QOP);
           if (saslQOP==null) {
              saslQOP = "auth-conf,auth-int,auth";
           }
           else {
              String[] temp = saslQOP.toLowerCase(englishLocale).split(",");
              String token;
              for (int i=0;i<temp.length;i++) {
                 token = temp[i].trim();
                 if (!(token.equals("auth")||token.equals("auth-int")||token.equals("auth-conf"))) {
                    throw new RuntimeException(token+" is not an acceptable digest-md5 protection option");
                 }
              }
           }

           digestMD5Enabled = Boolean.parseBoolean(properties.getProperty(SASL_DIGEST_MD5_ENABLE, "true"));
           if (digestMD5Enabled) {

              digestMD5Ciphers = properties.getProperty(SASL_DIGEST_MD5_CIPHERS);
              if (digestMD5Ciphers==null) {
                 digestMD5Ciphers = "3des,rc4";
              }
              else {
                 String[] temp = digestMD5Ciphers.toLowerCase(englishLocale).split(",");
                 String token;
                 for (int i=0;i<temp.length;i++) {
                    token = temp[i].trim();
                    if (!token.equals("3des")||!token.equals("des")||!token.equals("rc4-40")
                          ||!token.equals("rc4")||!token.equals("rc4-56")) {
                       throw new RuntimeException(token+" is not an acceptable digest-md5 cipher option");
                    }
                 }
              }
           }

           gssEnabled = Boolean.parseBoolean(properties.getProperty(SASL_GSS_ENABLE, "false"));
           if (gssEnabled) {

              gssRealm = properties.getProperty(SASL_GSS_REALM);
              if (gssRealm==null) {
                 throw new RuntimeException("A Kerberos 5 realm has to be defined");
              }
              gssRealm = gssRealm.toUpperCase(englishLocale);

              gssKDC = properties.getProperty(SASL_GSS_KDC);
              if (gssKDC==null) {
                 throw new RuntimeException("A Kerberos 5 kdc server hostname has to be defined");
              }

              gssOptions = new HashMap<String,String>(5,0.75f);

              String principal = properties.getProperty(SASL_GSS_PRINCIPAL);
              if (principal==null) {
                 throw new RuntimeException("A Kerberos 5 principal has to be defined");
              }
              if (principal.indexOf('/')==-1||principal.charAt(0)=='/') {
                 principal = "smtp:pop/"+principal.substring(principal.charAt(0)=='/'?1:0);
              }
              else {
                 if (principal.indexOf(':')==-1) {
                    String protocol = principal.substring(0, principal.indexOf('/'));
                    principal = protocol+":"+protocol+principal.substring(principal.indexOf('/'));
                 }
                 else {
                    String smtp = null, pop = null;
                    if (principal.charAt(0)==':') {
                       smtp = "smtp";
                       pop = principal.substring(principal.indexOf(':')+1,principal.indexOf('/'));
                    }
                    else if (principal.charAt(principal.indexOf('/')-1)==':'){
                       smtp = principal.substring(0,principal.indexOf(':'));
                       pop = "pop";
                    }
                    else {
                       smtp = principal.substring(0,principal.indexOf(':'));
                       pop = principal.substring(principal.indexOf(':')+1,principal.indexOf('/'));
                    }
                    principal = smtp+":"+pop+principal.substring(principal.indexOf('/'));

                 }
              }
              if (log.isDebugEnabled()) {
                 log.debug(principal);
              }
              gssOptions.put("principal", principal);
              gssPrincipal = principal;

              gssOptions.put("storeKey", Boolean.valueOf(properties.getProperty(SASL_GSS_STOREKEY, "true")).toString());

              Boolean useKeyTab = Boolean.valueOf(properties.getProperty(SASL_GSS_USEKEYTAB, "false"));
              if (useKeyTab) {
                 gssOptions.put("useKeyTab", useKeyTab.toString());
                 String keyTab = properties.getProperty(SASL_GSS_KEYTAB);
                 if (keyTab==null) {
                    throw new RuntimeException("A Kerberos 5 keyTab filename is required if useKeyTab is set to true");
                 }
                 gssOptions.put("keyTab", keyTab);
              }

           }
        }

        mime8bit = Boolean.parseBoolean(properties.getProperty(MIME8BIT, "true"));

        nonExistentLocalRejected = Boolean.valueOf(properties.getProperty( REJECT_NON_EXISTENT_LOCAL, "false").trim()).booleanValue();
        
        maxErrorCount = Integer.parseInt(properties.getProperty( MAX_ERROR_COUNT , "10").trim());
        if (maxErrorCount<10) maxErrorCount = 10;

        try {
           maxValidRCPT = Integer.parseInt(properties.getProperty( MAX_VALID_RCPT , "100").trim());
           if (maxValidRCPT<100) maxValidRCPT = 100;
        }
        catch (Exception e) {
           maxValidRCPT = 100;
        }

        try {
           addPctRCPT = Integer.parseInt(properties.getProperty( ADD_PCT_RCPT , "10").trim());
           if (addPctRCPT<5 || addPctRCPT>50) addPctRCPT = 5;
        }
        catch (Exception e) {
           addPctRCPT = 10;
        }

        try {
           minTotFailRCPT = Integer.parseInt(properties.getProperty( MIN_TOT_FAIL_RCPT , "20").trim());
           if (minTotFailRCPT<20) minTotFailRCPT = 20;
        }
        catch (Exception e) {
           minTotFailRCPT = 20;
        }

        try {
           minPctFailRCPT = Integer.parseInt(properties.getProperty( MIN_PCT_FAIL_RCPT , "90").trim());
           if (minPctFailRCPT<70 || minPctFailRCPT>100) minPctFailRCPT = 70;
        }
        catch (Exception e) {
           minPctFailRCPT = 90;
        }

        try {
           maxPassAttempts = Integer.parseInt(properties.getProperty( MAX_PASS_ATTEMPTS , "0").trim());
           if (maxPassAttempts<3 && maxPassAttempts!=0) maxPassAttempts = 3;
        }
        catch (Exception e) {
           maxPassAttempts = 0;
        }

        rcptPolicyMap = getRcptPolicyMap(loadRecipientPolicyFile(), englishLocale, backEnd);

        //Load the number of execute threads to use for each ServiceListener.
        String threadsString = properties.getProperty( EXECUTE_THREADS, "5" ).trim();
        try {
            executeThreadCount = Integer.parseInt( threadsString );
        }
        catch( NumberFormatException nfe ) {
            log.warn( "Invalid value for property: " + EXECUTE_THREADS + ".  Using default value of 5.");
            executeThreadCount = 5;
        }
        
        //Load the number of secure execute threads to use for each ServiceListener.
        String securethreadsString = properties.getProperty( SECURE_EXECUTE_THREADS, "2" ).trim();
        try {
            secureExecuteThreadCount = Integer.parseInt( securethreadsString );
        }
        catch( NumberFormatException nfe ) {
            log.warn( "Invalid value for property: " + EXECUTE_THREADS + ".  Using default value of 2.");
            secureExecuteThreadCount = 2;
        }

        String checkSuppliedStringAddress;
        InetAddress checkSuppliedInetAddress;

        //
        // Load the JES listen address
        //
        checkSuppliedStringAddress = properties.getProperty(LISTEN_ADDRESS);
        if (checkSuppliedStringAddress==null) {
           listenAddress = null;
           log.info("JES is listening on 0.0.0.0");
        }
        else {
           checkSuppliedStringAddress = checkSuppliedStringAddress.trim().toLowerCase(englishLocale);
           if (checkSuppliedStringAddress.equals("localhost") || checkSuppliedStringAddress.equals("127.0.0.1")) {
              checkSuppliedStringAddress = null;
           }
           try {
              checkSuppliedInetAddress = InetAddress.getByName(checkSuppliedStringAddress);
           }
           catch ( UnknownHostException unknownHostException ) {
              throw new RuntimeException( "Invalid value for property: " + LISTEN_ADDRESS );
           }
           if (checkSuppliedInetAddress.isLoopbackAddress() || checkSuppliedInetAddress.isSiteLocalAddress() ||
                 checkSuppliedInetAddress.isAnyLocalAddress() || externalDelegated) {
              listenAddress = checkSuppliedInetAddress;
              log.info("JES is listening on : "+listenAddress.getHostAddress());
           }
           else throw new RuntimeException("Address is not a zero, loopback or site local address: " + checkSuppliedStringAddress);
        }

        if (!fixedDefined) {
           
           securityManagerEnabled = Boolean.parseBoolean(properties.getProperty( ENABLE_SECURITYMANAGER, "false" ));
           //
           // Load the JES configuration address/port/security
           //
           configurationSecure = Boolean.parseBoolean(properties.getProperty(CONFIG_SECURE, "false"));

           checkSuppliedStringAddress = properties.getProperty(CONFIG_ADDRESS);
           if (checkSuppliedStringAddress==null) {
              configurationAddress = null;
           }
           else {
              checkSuppliedStringAddress = checkSuppliedStringAddress.trim().toLowerCase(englishLocale);
              if (checkSuppliedStringAddress.equals("localhost") || checkSuppliedStringAddress.equals("127.0.0.1")) {
                 checkSuppliedStringAddress = null;
              }
              try {
                 checkSuppliedInetAddress = InetAddress.getByName(checkSuppliedStringAddress);
                 if (checkSuppliedInetAddress!=InetAddress.getByName(null)) {
                    String[] checkForDomainName = null;
                    if (checkSuppliedStringAddress.indexOf('.')!=-1) {
                       checkForDomainName = checkSuppliedStringAddress.split("\\.");
                    }
                    else {
                       checkForDomainName = checkSuppliedStringAddress.split(":");
                    }
                    for (int i=checkForDomainName.length-1;i>=0;i--) {
                       if (checkForDomainName.length==0) continue;
                       try {
                          Integer.parseInt(checkForDomainName[i]);
                       } catch (NumberFormatException numberFormatException) {
                          throw new RuntimeException("No domain names allowed for configurator address: "+checkSuppliedStringAddress);
                       }
                    }
                 }
                 if (checkSuppliedInetAddress.isLoopbackAddress() || checkSuppliedInetAddress.isSiteLocalAddress()) {
                    configurationAddress = checkSuppliedInetAddress;
                    configurationPort = parsePort(properties.getProperty(CONFIG_PORT),41001);
                    if (configurationPort<0 || configurationPort>65535) configurationPort = 41001;
                    mappedPorts.put("config", configurationPort);
                 }
                 else throw new RuntimeException("Address is not a loopback or site local address: " + checkSuppliedStringAddress);
              }
              catch ( UnknownHostException unknownHostException ) {
                 throw new RuntimeException( "Invalid value for property: " + checkSuppliedStringAddress );
              }
           }

           standardSMTPsecure = Boolean.parseBoolean(properties.getProperty( STANDARDSMTPSECURE, "false").trim());
           standardPOP3secure = Boolean.parseBoolean(properties.getProperty( STANDARDPOP3SECURE, "false").trim())&&(retrievalMode==RetrievalMode.POP3);
           secureActive = Boolean.parseBoolean(properties.getProperty( SECUREACTIVE, "false").trim());
           String[] actString = properties.getProperty( ALLOWCLEARTEXT, "SMTP:encryptedOnly,POP3:encryptedOnly").trim().
                 toLowerCase(englishLocale).split(",");
           if (actString.length!=2 || !actString[0].startsWith("smtp") || !actString[1].startsWith("pop3")) {
              throw new RuntimeException( "Value should be of the form: \"SMTP:<option>,POP3:<option>\"" );
           }
           String smtpOption = actString[0].substring(5).toLowerCase();
           if (smtpOption.equals("always")) {
              allowClearTextSMTP = ALWAYS;
           }
           else if (smtpOption.equals("never")) {
              allowClearTextSMTP = NEVER;
           }
           else {
              allowClearTextSMTP = ENCRYPTEDONLY;
           }
           String pop3Option = actString[1].substring(5).toLowerCase();
           if (pop3Option.equals("always")) {
              allowClearTextPOP3 = ALWAYS;
           }
           else if (pop3Option.equals("never")) {
              allowClearTextPOP3 = NEVER;
           }
           else {
              allowClearTextPOP3 = ENCRYPTEDONLY;
           }
           clientAuth = properties.getProperty("client-auth", "no").trim().toLowerCase(englishLocale);
           boolean clientauthVerify = false;
           if (clientAuth.equals("required") || clientAuth.equals("requested") || clientAuth.equals("no")) clientauthVerify = true;
           if (!clientauthVerify) {
              clientAuth = "no";
           }

            if (standardSMTPsecure||standardPOP3secure||secureActive||outgoingSecure || configurationSecure) {

               if (isDebugSSL()) {
                  System.setProperty("javax.net.debug", "all");
                  log.info("Javax.net.debug set.");
               }
               else {
                  log.info("SSL debugging not requested. Javax.net.debug not set.");
               }

               FileInputStream fis = null;
               try {

                  //KeyStore setup
                  KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
                  File keystore = new File(keystoreLocation);
                  if (!keystore.exists()) {
                     if (log.isDebugEnabled()) {
                        log.debug("KeyStore file "+keystore.getPath()+" not found. Switching to default.");
                     }
                     keystoreLocation = getSecurityDirectory()+File.separator+"keystore.jceks";
                     keystore = new File(keystoreLocation);
                  }
                  if (keystore.exists()) {
                     if (log.isDebugEnabled()) {
                        log.debug("KeyStore file "+keystore.getPath()+" located.");
                     }
                     //A password is needed, check if one exists in the passwords map
                     if (!pwds.containsKey("keystore")) {
                        //Not present in passwords file, will require a gui
                        //If the jvm is headless throw exception
                        if (java.awt.GraphicsEnvironment.isHeadless()) {
                           log.error("The JVM is in headless mode but a GUI is required");
                           throw new RuntimeException("A GUI is required");
                        }
                        passwordpopup();
                        if (abort) {
                           throw new RuntimeException("password input aborted");
                        }
                     }
                     KeyStore ks = KeyStore.getInstance(getKeystoreType(), getKeystoreProvider());
                     fis = new FileInputStream(new File(getKeystoreLocation()));
                     ks.load(fis, pwds.get("keystore"));
                     kmf.init(ks, pwds.get("keystore"));
                     fis.close();
                     fis = null;
                     log.info("KeyManagerFactory successfully initialized.");
                  }
                  else {
                     if (log.isDebugEnabled()) {
                        log.debug("KeyStore file "+keystore.getPath()+" again not found.");
                     }
                     kmf.init(null, null);
                     log.info("No keystore file found.");
                  }

                  //TrustStore Setup
                  TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
                  File truststore = new File(truststoreLocation);
                  if (truststore.exists()) {
                     if (log.isDebugEnabled()) {
                        log.debug("TrustStore file "+truststore.getPath()+" located.");
                     }
                     KeyStore ks = KeyStore.getInstance(getTruststoreType(), getTruststoreProvider());
                     fis = new FileInputStream(new File(getTruststoreLocation()));
                     ks.load(fis, "password".toCharArray());
                     tmf.init(ks);
                     fis.close();
                     fis = null;
                  }
                  else {
                     if (log.isDebugEnabled()) {
                        log.debug("Unable to locate trustStore file "+truststore.getPath()+". Using default JSSE initiliazation method.");
                     }
                     tmf.init((KeyStore)null);
                  }
                  log.info("TrustManagerFactory successfully initialized.");

                  sslContext = SSLContext.getInstance("TLS");
                  sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
                  log.info("SSLContext successfully initialized.");

               }
               catch (Exception e) {
                  throw new RuntimeException(e.getMessage());
               }
               finally {
                  if (fis!=null) {
                     try {
                        fis.close();
                     }
                     catch(IOException ioe){}
                  }
               }
            }

           //amavisd-new setup
           if (amavisSupportActive = Boolean.parseBoolean( properties.getProperty( ACTIVATE_AMAVISD, "false" ).trim() ) ) {
              String amavisSMTPDirectory = properties.getProperty( AMAVISD_INCOMING_DIRECTORY, "" ).trim();
              if (!amavisSMTPDirectory.equals("") && !amavisSMTPDirectory.equals(instance.getSMTPDirectory())) {
                 File amavisDirectory = new File(amavisSMTPDirectory);
                 if (!amavisDirectory.exists()) {
                    amavisDirectory.mkdir();
                 }
                 instance.setAmavisSMTPDirectory(amavisSMTPDirectory);
                 if (log.isDebugEnabled()) {
                    log.debug("Setting Transmitting MTA's directory to "+amavisSMTPDirectory);
                 }
                 System.setProperty("jes.amavis.directory", amavisSMTPDirectory);
                 //
                 // Load the address JES will listen on for incoming traffic from amavisd-new
                 //
                 checkSuppliedStringAddress = properties.getProperty( AMAVISD_LISTEN_ADDRESS, listenAddress.getHostAddress() );
                 if (checkSuppliedStringAddress==null) {
                    amavisListenAddress = null;
                 }
                 else {
                    checkSuppliedStringAddress = checkSuppliedStringAddress.trim().toLowerCase(englishLocale);
                    if (checkSuppliedStringAddress.equals("localhost") || checkSuppliedStringAddress.equals("127.0.0.1")) {
                       checkSuppliedStringAddress = null;
                    }
                    try {
                       checkSuppliedInetAddress = InetAddress.getByName(checkSuppliedStringAddress);
                       if (checkSuppliedInetAddress.isLoopbackAddress() || checkSuppliedInetAddress.isSiteLocalAddress() ||
                             checkSuppliedInetAddress.isAnyLocalAddress() || externalDelegated) {
                          amavisListenAddress = checkSuppliedInetAddress;
                       }
                       else throw new RuntimeException("Address is not a zero, loopback or site local address: " + checkSuppliedStringAddress);
                    }
                    catch ( UnknownHostException unknownHostException ) {
                       throw new RuntimeException( "Invalid value for property: " +checkSuppliedStringAddress + ".  Server will listen on all addresses." + unknownHostException );
                    }
                 }

                 amavisSMTPPort = parsePort( properties.getProperty( AMAVISD_SMTPPORT ), 10024 );
                 amavisFilteredSMTPPort = parsePort( properties.getProperty( AMAVISD_FILTERED_SMTPPORT ), 10025 );

                 mappedPorts.put("amavis", amavisFilteredSMTPPort);

                 log.info("Successfully setup JES as a dual MTA. Amavis is listening on "+(amavisListenAddress==null?"0.0.0.0":amavisListenAddress.getHostAddress())+":"+amavisSMTPPort+", JES is listening on "+(listenAddress==null?"0.0.0.0":listenAddress.getHostAddress())+":"+amavisFilteredSMTPPort);

              }
              else {
                 amavisSupportActive = false;
                 if(amavisSMTPDirectory.equals(instance.getSMTPDirectory())) {
                    log.error("The Transmiting MTA can't share the same directory with the Receiving MTA.");
                 }
                 else {
                    log.info("Amavis support not requested.");
                 }
              }
           }
           else {
              log.info("Amavis support not requested.");
           }

           //Verify IP System setup
           if (verifyIP = Boolean.parseBoolean(properties.getProperty(VERIFY_IP, "false"))) {
              verifyIP = VerifyIPConfigurator.initializeVerifyIPConfigurator(rootDirectory);
           }

           updatePorts(properties);
           
           enableHELO = Boolean.parseBoolean(properties.getProperty( ENABLE_HELO, "false").trim());
           enableHELO = enableHELO&&!(standardSMTPsecure||secureActive);
           enableHELO = enableHELO&&!(digestMD5Enabled||gssEnabled);

        }
        else {
           
           updatePorts(properties);
           Mail.getInstance().notifyChange();
        }
        
        //
        // Load the SMTP Delivery Parameters
        //

        enablePOPBeforeSMTP = Boolean.parseBoolean( properties.getProperty( RELAY_POP_BEFORE_SMTP, "false" ).trim() );
        // Initialize the timeout Minutes parameter
        String timoutString = properties.getProperty( RELAY_POP_BEFORE_SMTP_TIMEOUT, "10" ).trim();
        try {
            setAuthenticationTimeoutMinutes( Long.parseLong( timoutString ) );
        }
        catch( NumberFormatException nfe ) {
            log.warn( "Invalid value for property: " + RELAY_POP_BEFORE_SMTP_TIMEOUT + ". Defaulting to 10." );
            //Set the default to 10 minutes.
            setAuthenticationTimeoutMinutes( 10 );
        }

        // Relay approved IP Addresses
        String ipAddresses = properties.getProperty( RELAY_ADDRESSLIST, "" ).trim();
        setRelayApprovedIPAddresses( tokenize( ipAddresses, true ) );

        // Relay approved email Addresses
        String emailAddresses = properties.getProperty( RELAY_EMAILSLIST, "" ).trim();
        setRelayApprovedEmailAddresses( tokenize( emailAddresses, true ) );

        // Load default Server info.

        String smtpServers = properties.getProperty( DEFAULT_SMTP_SERVERS, "" ).trim();
        if( smtpServers.length() > 0 ) {
            defaultSMTPServerEnabled = true;
            
            String[] raw = tokenize( smtpServers, true );
            defaultSMTPServers = new DefaultSMTPServer[raw.length];
            for (int i=0; i<raw.length; i++) {
                String server, credentials;
                
                int slash = raw[i].indexOf('/');
                if (slash == -1) {
                    server = raw[i];
                    credentials = null;
                }
                else {
                    server = raw[i].substring(0, slash);
                    credentials = raw[i].substring(slash + 1);
                }
                
                defaultSMTPServers[i] = new DefaultSMTPServer();
                defaultSMTPServers[i].setPort(25);
                int colon = server.indexOf(':');
                if (colon == -1) {
                    defaultSMTPServers[i].setHost(server);
                }
                else {
                    defaultSMTPServers[i].setHost(server.substring(0, colon));
                    defaultSMTPServers[i].setPort(Integer.parseInt(server.substring(colon + 1)));
                }
                if (defaultSMTPServers[i].getHost().length() == 0)
                    defaultSMTPServers[i].setHost("localhost");
                
                if (credentials != null) {
                    colon = credentials.indexOf(':');
                    defaultSMTPServers[i].setUsername(credentials.substring(0, colon));
                    defaultSMTPServers[i].setPassword(credentials.substring(colon + 1));
                }
            }
        }
        else {
            defaultSMTPServerEnabled = false;
            defaultSMTPServers = new DefaultSMTPServer[0];
        }

        String deliveryIntervalString = properties.getProperty( SMTP_DELIVERY_INTERVAL, "10" ).trim();
        try {
            //Convert to number and then convert to ms.
            setDeliveryIntervalSeconds( Long.parseLong( deliveryIntervalString ) );
        }
        catch( NumberFormatException nfe ) {
            setDeliveryIntervalSeconds( 10 );
        }

        // Set the Delivery Attempt Threshold.
        try
        {
            deliveryAttemptThreshold = Integer.parseInt(properties.getProperty( SMTP_DELIVERY_THRESHOLD, "10" ).trim() );
        }
        catch( NumberFormatException numberFormatException )
        {
            log.warn( "Invalid value for property: " + SMTP_DELIVERY_THRESHOLD + ". Defaulting to 10." );
            deliveryAttemptThreshold = 10;
        }

        // Set the Maximum message Size
        try
        {
            maximumMessageSize = Integer.parseInt( properties.getProperty( SMTP_MAX_MESSAGE_SIZE, "5" ).trim() );
            if ( maximumMessageSize < 1) maximumMessageSize = 1;
        }
        catch( NumberFormatException numberFormatException )
        {
            log.warn( "Invalid value for property: " + SMTP_MAX_MESSAGE_SIZE + ". Defaulting to 5." );
            deliveryAttemptThreshold = 5;
        }

        if (!fixedDefined) {

           fixedDefined = true;
           notifyDefault = Boolean.parseBoolean(properties.getProperty(NOTIFY_DEFAULT, "false"));

           if (notifyDefault) {

              if (!getDefaultMailbox(backEnd.getDefaultDomain().getDomainName()).isNULL()) {
                 disableWarning = Boolean.parseBoolean(properties.getProperty(DISABLE_WARNING, "false"));
              }
           }

           testingDirectory = properties.getProperty( TESTING_DESTINATION, "").trim();
           if (!testingDirectory.equals("")) {
              localTestingMode = true;
              transferMode = TransferMode.TESTING;
              requestDirCreation("testing:"+testingDirectory);
              System.setProperty("jes.testing.directory", instance.getTestingDirectory());
           }
           else {
              System.setProperty("jes.testing.directory", instance.getSMTPDirectory());
           }
        }
        
        // Update the 'last loaded' timestamp.
        generalConfigurationFileTimestamp = generalConfigurationFile.lastModified();
    }
    
    public static Map<Object,RcptPolicyArray<String>> getRcptPolicyMap(Properties rcptPolicyFileEntries, Locale locale, ConfigurationManagerBackEnd backEnd) {
       
        Map<Object,RcptPolicyArray<String>> rcptPolicyMap = new ConcurrentHashMap<Object,RcptPolicyArray<String>>();

        Enumeration enumeration = rcptPolicyFileEntries.keys();
        String key;
        Properties lowerCased = new Properties();
        while(enumeration.hasMoreElements()) {
           key = (String)enumeration.nextElement();
           lowerCased.put(key.toLowerCase(locale), removeWSFromString(rcptPolicyFileEntries.getProperty(key)));
        }

        rcptPolicyFileEntries = lowerCased;
        
        boolean containsGlobalAllow = false;
        boolean containsGlobalBlock = false;
        if (rcptPolicyFileEntries.containsKey("allow")) containsGlobalAllow = true;
        if (rcptPolicyFileEntries.containsKey("block")) containsGlobalBlock = true;
        
        if (!containsGlobalAllow) {
           if (!containsGlobalBlock) {
              RcptPolicyArray<String> single = new RcptPolicyArray<String>(true);
              single.add("ALL");
              rcptPolicyMap.put("#####", single);
           }
           else {
              rcptPolicyMap.put("#####", new RcptPolicyArray<String>(rcptPolicyFileEntries.getProperty("block").toLowerCase(locale).split(","), false));
           }
        }
        else {
           if (!containsGlobalBlock) {
              rcptPolicyMap.put("#####", new RcptPolicyArray<String>(rcptPolicyFileEntries.getProperty("allow").toLowerCase(locale).split(","), true));
           }
           else {
              rcptPolicyMap.put("#####", new RcptPolicyArray<String>(rcptPolicyFileEntries.getProperty("block").toLowerCase(locale).split(","), false));
           }
        }
        
        Set rcptPolicyKeys = rcptPolicyFileEntries.keySet();
        Iterator iter = rcptPolicyKeys.iterator();
        Domain domainKey;
        while(iter.hasNext()) {
           key = (String)iter.next();
           if (key.equals("allow")||key.equals("block")) continue;
           domainKey = new Domain(key.substring(key.indexOf('.')+1));
           if (backEnd!=null&&!backEnd.isLocalDomain(domainKey)) continue;
           if (key.startsWith("allow")) {
              if (rcptPolicyKeys.contains("block."+domainKey)) continue;
              else {
                 rcptPolicyMap.put(domainKey, new RcptPolicyArray<String>(rcptPolicyFileEntries.getProperty(key).toLowerCase(locale).split(","), true));
              }
           }
           else if (key.startsWith("block")) {
              rcptPolicyMap.put(domainKey, new RcptPolicyArray<String>(rcptPolicyFileEntries.getProperty(key).toLowerCase(locale).split(","), false));
           }
        }
        return rcptPolicyMap;
    }
    
    private static String removeWSFromString(String text) {
       StringBuilder sb = new StringBuilder(text.length());
       for(char c:text.toCharArray()) {
          if (c == 0x20) continue;
          sb.append(c);
       }
       return sb.toString();
    }

    private void updatePorts(Properties properties) {

       mappedPorts.remove("smtp");
       mappedPorts.remove("pop3");
       mappedPorts.remove("secureSmtp");
       mappedPorts.remove("securePop3");

       smtpPort = parsePort( properties.getProperty( SMTPPORT ), 25 );
       pop3Port = parsePort( properties.getProperty( POP3PORT ), 110 );
       if (smtpPort<0 || smtpPort>65535) smtpPort = 25;
       if (pop3Port<0 || pop3Port>65535) pop3Port = 110;

       Collection<Integer> mappings = mappedPorts.values();
       if (mappings.contains(smtpPort)&&smtpPort!=25) {
          smtpPort = 25;
       }
       while(mappings.contains(smtpPort)) {
          smtpPort = getRandomPort();
       }
       mappedPorts.put("smtp", smtpPort);
       
       mappings = mappedPorts.values();
       if (mappings.contains(pop3Port)&&pop3Port!=110) {
          pop3Port = 110;
       }
       while(mappings.contains(pop3Port)) {
          pop3Port = getRandomPort();
       }
       mappedPorts.put("pop3", pop3Port);

       if (secureActive) {
          secureSMTPPort = parsePort( properties.getProperty( SECURESMTPPORT ), 465 );
          if (secureSMTPPort<0 || secureSMTPPort>65535) secureSMTPPort = 465;

          mappings = mappedPorts.values();
          if (mappings.contains(secureSMTPPort)&&secureSMTPPort!=465) {
             secureSMTPPort = 465;
          }
          while(mappings.contains(secureSMTPPort)) {
             secureSMTPPort = getRandomPort();
          }
          mappedPorts.put("secureSmtp", secureSMTPPort);

          if (retrievalMode==RetrievalMode.POP3) {

             securePOP3Port = parsePort( properties.getProperty( SECUREPOP3PORT ), 995 );
             if (securePOP3Port<0 || securePOP3Port>65535) securePOP3Port = 995;

             mappings = mappedPorts.values();
             if (mappings.contains(securePOP3Port)&&securePOP3Port!=995) {
                securePOP3Port = 995;
             }
             while(mappings.contains(securePOP3Port)) {
                securePOP3Port = getRandomPort();
             }
             mappedPorts.put("securePop3", securePOP3Port);
          }
       }

       try {
          ((JESSecurityManager)System.getSecurityManager()).updateMappedPorts();
       }
       catch (NullPointerException npe) {
          //A security manager may not be active yet
       }
    }
    
    private int getRandomPort() {
       return 10500+new Random().nextInt(55035);
    }

    private void passwordpopup() {
       //password pop up window
       java.awt.EventQueue.invokeLater(new Runnable() {

          public void run() {
             PasswordPopup ppu = new PasswordPopup();
             ppu.pack();
             ppu.setLocationRelativeTo(null);
             ppu.setVisible(true);
          }
       });
       while(!complete) {
          try {
             Thread.sleep(0,500);
          } catch (InterruptedException ex) {
          }
       }
    }

    private Properties loadRecipientPolicyFile() {

        DelimitedInputStream dis = null;
        try {
            dis = new DelimitedInputStream(new FileInputStream( rcptPolicyFile ),2048);
            JESProperties jesProperties = new JESProperties(dis);
            jesProperties.load();
            return jesProperties.getProperties();
        }
        catch (IOException e) {
            // All checks should be done before we get here, so there better
            // not be any errors.  If so, throw a RuntimeException.
            throw new RuntimeException( "Error Loading Recipient Policy File!  Unable to continue Operation." );
        }
        finally {
           if (null!=dis) {
              try {
                 dis.close();
              } catch (IOException ex) {
                 if (log.isDebugEnabled()) {
                    log.debug(ex.getMessage());
                 }
              }
           }
        }
    }
    
    public void persistUpdates() {

       createDirectories();
       backEnd.persistUsersAndRealms();
       doBackup();
       if (!running) {
          
           running = true;
           // Start the Watchdog Thread
           watcher = instance.new ConfigurationFileWatcher();
           watcher.start();
       }
    }
    
    private void loadreverseDNSservers() {
       
       BufferedReader bufferedReader = null;
        try {
            int count = 0;
            bufferedReader = new BufferedReader(new FileReader( reverseDNSFile ));
            do{
               String server = bufferedReader.readLine();
               if (server == null) break;
               if (!(server.charAt(0) == 0x23)) count++;
            }while(true);
            if (count>0) {
               bufferedReader.close();
               reverseDNSservers = new String[count];
               bufferedReader = new BufferedReader(new FileReader( reverseDNSFile ));
               count = 0;
               do{
                  String server = bufferedReader.readLine();
                  if (server == null) break;
                  if (!(server.charAt(0) == 0x23))
                     reverseDNSservers[count++] = server.trim();
               }while(true);
            }
            else {
               reverseDNSservers = new String[0];
            }
        }
        catch (IOException e) {
            // All checks should be done before we get here, so there better
            // not be any errors.  If so, throw a RuntimeException.
            throw new RuntimeException( "Error Loading reverse DNS servers File!  Unable to continue Operation." );
        }
        finally {
           if (bufferedReader!=null) {
              try {
                 bufferedReader.close();
              } catch (IOException ex) {
                 if (log.isDebugEnabled()) {
                    log.debug(ex.getMessage());
                 }
              }
           }
        }
        
        if( log.isInfoEnabled() ) log.info( "Loaded " + reverseDNSservers.length + " servers from reverseDNS.conf" );
        
        // Update the 'last loaded' timestamp.
        reverseDNSFileTimestamp = reverseDNSFile.lastModified();
    }

    /**
     * Loads the values of the specified key from the configuration file.
     * This method parses the value into a String array
     * using the comma (,) as a delimiter.  This method returns an
     * array of size 0 if the the value string was null or empty.
     *
     * @param value the string to tokenize into an array.
     * @return a String[] of the values, or an empty array if the key could not be found.
     */
    public static String[] tokenize( String value, boolean clearWhiteSpace ) {

        if( value == null || value.trim().equals( "" ) ) {
            return new String[0];
        }
        else {
            String[] result = value.split( "," );
            if (clearWhiteSpace) {
               for (int i=0;i<result.length;i++) {
                  result[i] = result[i].trim();
               }
            }
            return result;
        }
    }

    /**
     * Converts the string into a valid port number.
     *
     * @param stringValue the string value to parse
     * @param defaultValue the default value to return if parsing fails.
     * @return a valid int.
     */
    private int parsePort( String stringValue, int defaultValue ) {
        int value = defaultValue;
        if( stringValue != null && stringValue.length() > 0 ) {
            try {
                value = Integer.parseInt( stringValue );
            }
            catch (NumberFormatException e) {
                log.warn( "Error parsing port string: " + stringValue + " using default value: " + defaultValue );
            }
        }
        return value;
    }

    public void updateThroughConnection(CBCExecutor cbcExecutor) {

       backEnd.updateThroughConnection(cbcExecutor);
    }
    
    private void doBackup() {
       try {
          FileUtils.copyFile(new File(backupDirectory, "user.bak"), new File(backupDirectory, "user.ba2"));
          FileUtils.copyFile(new File(backupDirectory, "realms.bak"), new File(backupDirectory, "realms.ba2"));
          FileUtils.copyFile(new File(backupDirectory, "realmpwd.bak"), new File(backupDirectory, "realmpwd.ba2"));
       }
       catch (IOException e) {
          if (log.isDebugEnabled()) {
            log.debug(e.getMessage());
          }
       }
       try {
          backEnd.doBackup(backupDirectory);
          FileUtils.copyFile(new File(securityDirectory, "realms.dat"), new File(backupDirectory, "realmpwd.bak"));
       }
       catch (IOException e) {
          if (log.isDebugEnabled()) {
            log.debug(e.getMessage());
          }
       }
    }

    private void doWeeklyBackup() {
       try {
          backEnd.doWeeklyBackup(backupDirectory);
          FileUtils.copyFile(new File(securityDirectory, "realms.dat"), new File(backupDirectory, "realmpwd.wek"));
       }
       catch (IOException e) {
          if (log.isDebugEnabled()) {
            log.debug(e.getMessage());
          }
       }
    }

    //***************************************************************
    // Watchdog Inner Class
    //***************************************************************

    /**
     * Checks the user configuration file and reloads it if it is new.
     */
    class ConfigurationFileWatcher extends Thread {

        /**
         * Initialize the thread.
         */
        public ConfigurationFileWatcher() {
            super( "Config Watchdog" );
            setDaemon( true );
        }

        /**
         * Check the timestamp on the file to see
         * if it has been updated.
         */
        public void run() {
            long sleepTime = 10 * 1000;
            boolean general = false, user = false, reverse = false;
            Calendar calendar;
            boolean doWeeklyBackup = false, doneWeeklyBackup = false;
            while( running ) {
                try {
                    Thread.sleep( sleepTime );
                }
                catch( InterruptedException ie) {
                   Thread.interrupted();
                   if (!running) {
                      break;
                   }
                }
                while(updatingFiles) {
                   try {
                      Thread.sleep( 5000 );
                   }
                   catch( InterruptedException ie) {
                      Thread.interrupted();
                      if (!running) {
                         break;
                      }
                   }

                }

                updatingFiles = true;
                 if( generalConfigurationFile.lastModified() > generalConfigurationFileTimestamp ) {
                     log.info( "General Configuration File Changed, reloading..." );
                     general = true;
                 }
                user = backEnd.persistUserUpdate();
                 if( reverseDNSFile.lastModified() > reverseDNSFileTimestamp ) {
                     log.info( "reverse DNS servers File Changed, reloading..." );
                     reverse = true;
                 }
                if (general||user||reverse) {
                   int select = 0;
                   if (!disableNotify&&!disableWarning&&!java.awt.GraphicsEnvironment.isHeadless()) {
                      select = JOptionPane.showConfirmDialog(null, "Some configuration files have been updated, is it OK to reload ?",
                            "Confirm Update", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
                   }
                   disableNotify = false;

                   if (select==0&&(!disableWarning||disableNotify)) {
                      if (log.isDebugEnabled()) {
                         log.debug("reloading settings");
                      }
                      if (general) {
                         loadGeneralProperties();
                      }
                      if (reverse) {
                         loadreverseDNSservers();
                      }
                      if (user) {
                         backEnd.updateUsersAndRealmPasswords();
                         persistUpdates();

                      }
                   }
                   if (notifyDefault) {
                      notifyDefaultUser();
                   }
                }
                general = user = reverse = false;
                updatingFiles = false;
                
                calendar = Calendar.getInstance();
                if (calendar.get(Calendar.DAY_OF_WEEK)==Calendar.WEDNESDAY) {
                   doWeeklyBackup = true;
                }
                else if (calendar.get(Calendar.DAY_OF_WEEK)==Calendar.THURSDAY) {
                   doWeeklyBackup = false;
                   doneWeeklyBackup = false;
                }
                if (doWeeklyBackup&&!doneWeeklyBackup) {
                   File userWeek = new File(backupDirectory, "user.wek");
                   if (!userWeek.exists()) {
                      doWeeklyBackup();
                      doneWeeklyBackup = true;
                   }
                   else if (!doneWeeklyBackup) {
                      Calendar backupCalendar = Calendar.getInstance();
                      backupCalendar.setTimeInMillis(userWeek.lastModified());
                      if (backupCalendar.get(Calendar.DATE)!=calendar.get(Calendar.DATE)) {
                         doWeeklyBackup();
                      }
                      doneWeeklyBackup = true;
                   }
                }
            }
            generalConfigurationFileTimestamp = Long.MAX_VALUE;
            reverseDNSFileTimestamp = Long.MAX_VALUE;
        }

        private void notifyDefaultUser() {
           log.info( "Notifying the admin of request for updates" );

           SMTPMessage bounceMessage = new SMTPMessageImpl();

           String domain = backEnd.getDefaultDomain().getDomainName();

           EmailAddress toAddress = getDefaultMailbox(domain);
           if (toAddress.isNULL()) {
              return;
           }

           //Set the from address as mailserver@ the first (default) local domain.
           EmailAddress fromAddress = EmailAddress.getEmailAddress( "MAILER_DAEMON", domain );

           bounceMessage.setFromAddress( fromAddress );
           bounceMessage.addToAddress( toAddress );
           bounceMessage.addDataLine( string2Bytes("From: Mail Delivery Subsystem <MAILER-DAEMON@" + domain + ">") );
           bounceMessage.addDataLine( string2Bytes("To: " + toAddress ) );
           bounceMessage.addDataLine( string2Bytes("Subject: Request for updates") );
           bounceMessage.addDataLine( string2Bytes("Date: " + new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z").format(new Date())) );
           bounceMessage.addDataLine( string2Bytes("MIME-Version: 1.0") );
           bounceMessage.addDataLine( string2Bytes("Content-type: text/plain; charset=us-ascii") );
           bounceMessage.addDataLine( string2Bytes("") );
           bounceMessage.addDataLine( string2Bytes("mail/user.conf has changed. Please take appropriate action.") );

           //Save this message so it will be delivered.
           try {
               bounceMessage.getSMTPPersistenceProccessor().saveBegin(ConfigurationManager.getInstance().isAmavisSupportActive());
               bounceMessage.getSMTPPersistenceProccessor().saveIncrement(bounceMessage.getDataLines(), true, false);
               bounceMessage.getSMTPPersistenceProccessor().saveFinish();
           }
           catch (IOException ioe) {
               log.error( "Error storing outgoing 'bounce' email message");
               throw new RuntimeException(ioe);
           }
        }

        private byte[] string2Bytes( String line ) {
           try {
              return line.getBytes("US-ASCII");
           }
           catch (UnsupportedEncodingException uee) {
              return line.getBytes();
           }
        }
    }
}
